return TRUE;
}
+/* Generate a drop-in for a mount unit to set DefaultDependencies=no.
+ *
+ * By default, systemd auto-generates mount units from /proc/self/mountinfo with
+ * DefaultDependencies=yes, which includes Conflicts=umount.target. This causes
+ * the mount to be unmounted during soft-reboot shutdown. Since soft-reboot doesn't
+ * re-run the initramfs, these mounts are never remounted, breaking bootc/ostree.
+ *
+ * By generating a drop-in, we override just the DefaultDependencies setting
+ * while letting systemd handle the actual mount parameters (What=, etc.) from
+ * the existing mount in /proc/self/mountinfo.
+ *
+ * @mount_unit: The mount unit name (e.g., "sysroot.mount", "boot.mount")
+ * @mount_point: The mount point path for the comment (e.g., "/sysroot", "/boot")
+ */
+static gboolean
+generate_mount_unit_dropin (int normal_dir_dfd, const char *mount_unit, const char *mount_point,
+ GError **error)
+{
+ GCancellable *cancellable = NULL;
+
+ /* Create the drop-in directory (e.g., sysroot.mount.d) */
+ g_autofree char *dropin_dir = g_strdup_printf ("%s.d", mount_unit);
+ if (!glnx_shutil_mkdir_p_at (normal_dir_dfd, dropin_dir, 0755, cancellable, error))
+ return FALSE;
+
+ g_auto (GLnxTmpfile) tmpf = {
+ 0,
+ };
+ if (!glnx_open_tmpfile_linkable_at (normal_dir_dfd, ".", O_WRONLY | O_CLOEXEC, &tmpf, error))
+ return FALSE;
+ g_autoptr (GOutputStream) outstream = g_unix_output_stream_new (tmpf.fd, FALSE);
+ gsize bytes_written;
+
+ /* Generate drop-in to set DefaultDependencies=no.
+ *
+ * Key points:
+ * - DefaultDependencies=no prevents Conflicts=umount.target from being added
+ * - This allows the mount to survive soft-reboot
+ * - The actual mount (What=, Where=, etc.) comes from /proc/self/mountinfo
+ */
+ if (!g_output_stream_printf (outstream, &bytes_written, cancellable, error,
+ "##\n# Automatically generated by ostree-system-generator\n"
+ "# Preserve %s across soft-reboot\n##\n\n"
+ "[Unit]\n"
+ "DefaultDependencies=no\n"
+ "After=local-fs-pre.target\n"
+ "Before=local-fs.target\n",
+ mount_point))
+ return FALSE;
+ if (!g_output_stream_flush (outstream, cancellable, error))
+ return FALSE;
+ g_clear_object (&outstream);
+ if (!glnx_fchmod (tmpf.fd, 0644, error))
+ return FALSE;
+
+ g_autofree char *dropin_path = g_strdup_printf ("%s/ostree-softreboot.conf", dropin_dir);
+ if (!glnx_link_tmpfile_at (&tmpf, GLNX_LINK_TMPFILE_NOREPLACE, normal_dir_dfd, dropin_path,
+ error))
+ return FALSE;
+
+ return TRUE;
+}
+
+/* Generate a drop-in for sysroot.mount to preserve it across soft-reboot.
+ *
+ * /sysroot is special: it's mounted in the initramfs and can never be
+ * remounted without re-running the initramfs. Since soft-reboot skips
+ * the initramfs, we must prevent systemd from unmounting it.
+ */
+static gboolean
+sysroot_mount_generator (const char *normal_dir, GError **error)
+{
+ glnx_autofd int normal_dir_dfd = -1;
+
+ if (!glnx_opendirat (AT_FDCWD, normal_dir, TRUE, &normal_dir_dfd, error))
+ return FALSE;
+
+ if (!generate_mount_unit_dropin (normal_dir_dfd, "sysroot.mount", "/sysroot", error))
+ return FALSE;
+
+ return TRUE;
+}
+
+/* Generate boot.mount for /boot when it's on the same partition as /sysroot.
+ *
+ * When /boot is on a separate partition, systemd auto-generates a mount unit
+ * from /proc/self/mountinfo and will remount it after soft-reboot. No action
+ * needed in that case.
+ *
+ * When /boot is on the same partition (detected by /sysroot/boot/loader being
+ * a symlink), a bind mount from /sysroot/boot is needed. Previously this was
+ * done in the initramfs (ostree-prepare-root), but that breaks on bare
+ * soft-reboot since the initramfs doesn't re-run. By generating boot.mount
+ * here, the generator handles all three boot scenarios:
+ * 1. Normal boot (generator runs from initramfs)
+ * 2. Bare soft-reboot (generator re-runs)
+ * 3. Staged deployment soft-reboot (generator re-runs for new root)
+ *
+ * See: https://github.com/ostreedev/ostree/pull/3487
+ * https://github.com/ostreedev/ostree/pull/3571
+ */
+static gboolean
+boot_mount_generator (const char *normal_dir, GError **error)
+{
+ GCancellable *cancellable = NULL;
+ static const char boot_path[] = "/boot";
+ struct stat stbuf;
+
+ /* Check if /boot is on the same partition as /sysroot by looking for
+ * /sysroot/boot/loader as a symlink. This is the same check used by
+ * otcore_mount_boot() in the initramfs path.
+ */
+ if (!(lstat ("/sysroot/boot/loader", &stbuf) == 0 && S_ISLNK (stbuf.st_mode)))
+ return TRUE; /* /boot is a separate partition, systemd handles it */
+
+ /* Verify the target /boot directory exists */
+ if (!(lstat ("/boot", &stbuf) == 0 && S_ISDIR (stbuf.st_mode)))
+ return TRUE;
+
+ glnx_autofd int normal_dir_dfd = -1;
+ if (!glnx_opendirat (AT_FDCWD, normal_dir, TRUE, &normal_dir_dfd, error))
+ return FALSE;
+
+ g_auto (GLnxTmpfile) tmpf = {
+ 0,
+ };
+ if (!glnx_open_tmpfile_linkable_at (normal_dir_dfd, ".", O_WRONLY | O_CLOEXEC, &tmpf, error))
+ return FALSE;
+ g_autoptr (GOutputStream) outstream = g_unix_output_stream_new (tmpf.fd, FALSE);
+ gsize bytes_written;
+
+ /* Generate a boot.mount unit that bind-mounts /sysroot/boot to /boot.
+ *
+ * We use DefaultDependencies=no for the same reasons as var.mount:
+ * to avoid implicit device ordering that can stall after soft-reboot.
+ * Since this is a bind mount from /sysroot, we only need sysroot.mount.
+ */
+ if (!g_output_stream_printf (outstream, &bytes_written, cancellable, error,
+ "##\n# Automatically generated by ostree-system-generator\n"
+ "# Bind mount /boot from /sysroot/boot (same partition)\n##\n\n"
+ "[Unit]\n"
+ "Documentation=man:ostree(1)\n"
+ "DefaultDependencies=no\n"
+ "After=local-fs-pre.target sysroot.mount\n"
+ "Before=local-fs.target\n"
+ "\n"
+ "[Mount]\n"
+ "Where=%s\n"
+ "What=/sysroot/boot\n"
+ "Options=bind\n",
+ boot_path))
+ return FALSE;
+ if (!g_output_stream_flush (outstream, cancellable, error))
+ return FALSE;
+ g_clear_object (&outstream);
+ if (!glnx_fchmod (tmpf.fd, 0644, error))
+ return FALSE;
+ if (!glnx_link_tmpfile_at (&tmpf, GLNX_LINK_TMPFILE_NOREPLACE, normal_dir_dfd, "boot.mount",
+ error))
+ return FALSE;
+
+ /* Ensure it's pulled in by local-fs.target */
+ if (!glnx_shutil_mkdir_p_at (normal_dir_dfd, "local-fs.target.requires", 0755, cancellable,
+ error))
+ return FALSE;
+ if (symlinkat ("../boot.mount", normal_dir_dfd, "local-fs.target.requires/boot.mount") < 0)
+ return glnx_throw_errno_prefix (error, "symlinkat");
+
+ return TRUE;
+}
+
/* Generate var.mount */
static gboolean
fstab_generator (const char *ostree_target, const bool is_aboot, const char *normal_dir,
* Documentation/filesystems/sharedsubtree.txt and
* https://github.com/ostreedev/ostree/issues/2086. This also happens in
* ostree-prepare-root.c for the INITRAMFS_MOUNT_VAR case.
+ *
+ * We use DefaultDependencies=no to avoid implicit ordering dependencies that
+ * can cause the mount to stall after a bare `systemctl soft-reboot`. Without
+ * this, systemd may add implicit After= dependencies on device units, which
+ * can get stuck in 'tentative' state while udev restarts after soft-reboot.
+ * Since this is a bind mount from /sysroot (which survives soft-reboot), we
+ * only need to wait for sysroot.mount and local-fs-pre.target.
+ *
+ * Note: We intentionally do NOT add Conflicts=umount.target or
+ * Before=umount.target here. Adding those creates a dependency deadlock
+ * with ostree-remount.service during soft-reboot shutdown, because:
+ * - ostree-remount.service has After=var.mount and Before=local-fs.target
+ * - Adding Conflicts=umount.target to var.mount creates circular ordering
+ * where umount.target waits for ostree-remount.service, which waits for
+ * local-fs.target, which waits for var.mount
+ * Since /var survives soft-reboot (as a bind mount from /sysroot), systemd
+ * catches it up from mountinfo and it doesn't need special unmount handling.
+ *
+ * See https://issues.redhat.com/browse/RHEL-154075
*/
if (!g_output_stream_printf (outstream, &bytes_written, cancellable, error,
"##\n# Automatically generated by ostree-system-generator\n##\n\n"
"[Unit]\n"
"Documentation=man:ostree(1)\n"
+ "DefaultDependencies=no\n"
"ConditionKernelCommandLine=!systemd.volatile\n"
+ "After=local-fs-pre.target sysroot.mount\n"
"Before=local-fs.target\n"
"\n"
"[Mount]\n"
if (!require_internal_units (normal_dir, early_dir, late_dir, error))
return FALSE;
+ if (!sysroot_mount_generator (normal_dir, error))
+ return FALSE;
+ if (!boot_mount_generator (normal_dir, error))
+ return FALSE;
if (!fstab_generator (ostree_target, is_aboot, normal_dir, early_dir, late_dir, error))
return FALSE;
#!/bin/bash
+# shellcheck disable=SC2154 # host_commit is defined in libinsttest.sh
set -xeuo pipefail
-. ${KOLA_EXT_DATA}/libinsttest.sh
+. "${KOLA_EXT_DATA}"/libinsttest.sh
prepare_tmpdir
require_writable_sysroot
assert_soft_reboot_count() {
- assert_streq $(systemctl show -P SoftRebootsCount) $1
+ assert_streq "$(systemctl show -P SoftRebootsCount)" "$1"
}
case "${AUTOPKGTEST_REBOOT_MARK:-}" in
systemctl mask --now zincati
assert_soft_reboot_count 0
+
+ # First, test a bare systemctl soft-reboot (without ostree's prepare-soft-reboot).
+ # This tests the fix for https://issues.redhat.com/browse/RHEL-154075 where
+ # a bare soft-reboot would cause /var to fail to mount due to the generated
+ # var.mount unit getting stuck waiting on device units.
+ echo "Testing bare systemctl soft-reboot (no /run/nextroot)..."
+ # Verify /run/nextroot is not set up
+ test '!' -d /run/nextroot || ! mountpoint -q /run/nextroot
+ /tmp/autopkgtest-soft-reboot-prepare "bare-soft-reboot"
+ systemctl soft-reboot
+ ;;
+ "bare-soft-reboot")
+ # After bare soft-reboot, verify we're still running the same deployment
+ # and critically, that /var is mounted and the system is healthy.
+ echo "Verifying post-bare-soft-reboot state..."
+ assert_soft_reboot_count 1
+
+ # The key assertion: /var must be mounted for the system to be functional
+ mountpoint /var
+ # Verify /var is actually usable (we can write to it)
+ touch /var/tmp/soft-reboot-test-marker
+ rm /var/tmp/soft-reboot-test-marker
+
+ # /boot must also be mounted (handled by generator's boot.mount)
+ mountpoint /boot
+
+ # We should still be on the same deployment (no ostree-level change)
+ assert_status_jq '.deployments[0].booted'
+
+ echo "ok bare soft-reboot"
+
+ # Now continue with the rest of the soft-reboot tests
assert_status_jq '.deployments[0].pending | not' '.deployments[0].["soft-reboot-target"] | not'
# Create a synthetic commit for upgrade
cd /ostree/repo/tmp
- ostree checkout -H ${host_commit} t
+ ostree checkout -H "${host_commit}" t
unshare -m /bin/sh -c 'mount -o remount,rw /sysroot && cd /ostree/repo/tmp/t && touch usr/etc/new-file-for-soft-reboot usr/share/test-file-for-soft-reboot'
ostree commit --no-bindings --parent="${host_commit}" -b soft-reboot-test -I --consume t
- newcommit=$(ostree rev-parse soft-reboot-test)
+ ostree rev-parse soft-reboot-test >/dev/null
# Deploy the new commit normally first
ostree admin deploy --stage soft-reboot-test
"2")
# After soft reboot, verify we're running the new deployment
echo "Verifying post-soft-reboot state..."
- assert_soft_reboot_count 1
+ assert_soft_reboot_count 2
expected_commit=$(ostree rev-parse soft-reboot-test)
if [ "${host_commit}" != "${expected_commit}" ]; then
- echo "ERROR: Expected commit ${host_commit}, but got ${current_commit}"
+ echo "ERROR: Expected commit ${expected_commit}, but got ${host_commit}"
exit 1
fi
ostree admin prepare-soft-reboot --reboot 1
;;
"3")
- assert_soft_reboot_count 2
+ assert_soft_reboot_count 3
# Only from the first updated target
test '!' -f /etc/new-file-for-soft-reboot
# Now, test the intersection of staged deployments and soft rebooting
# Create another synthetic commit
cd /ostree/repo/tmp
- ostree checkout -H ${host_commit} t
+ ostree checkout -H "${host_commit}" t
unshare -m /bin/sh -c 'mount -o remount,rw /sysroot && cd /ostree/repo/tmp/t && touch usr/share/test-staged-2-for-soft-reboot'
ostree commit --no-bindings --parent="${host_commit}" -b soft-reboot-test-staged-2 -I --consume t
- newcommit=$(ostree rev-parse soft-reboot-test-staged-2)
+ ostree rev-parse soft-reboot-test-staged-2 >/dev/null
ostree admin deploy --stage soft-reboot-test-staged-2
assert_status_jq '.deployments[0].staged' '.deployments[0].["soft-reboot-target"] | not' \
systemctl reboot
;;
"4")
- assert_soft_reboot_count 3
+ assert_soft_reboot_count 4
# Completion of soft reboot into non-staged
assert_status_jq '.deployments[0].booted' '.deployments[0].["soft-reboot-target"] | not' \
'.deployments[1].booted | not' '.deployments[1].["soft-reboot-target"] | not'